/*
     Copyright 2007 Joseph M. Ferner

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using log4net;
using OpenServers.Net.Common.Socket;
using OpenServers.Net.Common.State;

namespace OpenServers.Net.Common.Listener {
    /// <summary>
    /// a listener which listens for TCP/IP connections on a nonsecure protocol.
    /// </summary>
    public class NonsecureTcpListener : IListener {
        private static readonly ILog log = LogManager.GetLogger(typeof(NonsecureTcpListener));
        private readonly int _port;
        private TcpListener _tcpListener;
        private ManualResetEvent _acceptThreadLoopEvent;
        private bool _acceptConnections;
        private Thread _acceptThreadLoop;
        private IServer _server;
        private bool _started;

        /// <summary>
        /// constructor.
        /// </summary>
        /// <param name="port">The port to listen on.</param>
        public NonsecureTcpListener(int port) {
            this._port = port;
        }

        /// <summary>
        /// Checks if the listener is started.
        /// </summary>
        public bool Started {
            get { return _started; }
        }

        /// <summary>
        /// Starts the listener.
        /// </summary>
        public void Start(IServer server) {
            if (!Started) {
                this._server = server;
                _tcpListener = new TcpListener(IPAddress.Any, this._port);
                _tcpListener.Start();
                _acceptThreadLoopEvent = new ManualResetEvent(false);
                _acceptConnections = true;
                _acceptThreadLoop = new Thread(AcceptThreadLoop);
                _acceptThreadLoop.Start();
                _started = true;
            }
        }

        private void AcceptThreadLoop(object obj) {
            while (_acceptConnections) {
                _acceptThreadLoopEvent.Reset();
                _tcpListener.BeginAcceptSocket(AcceptSocket, null);
                _acceptThreadLoopEvent.WaitOne();
            }
        }

        private void AcceptSocket(IAsyncResult ar) {
            IState state = null;
            try {
                ISocket clientSocket;
                try {
                    clientSocket = new TcpSocket(_tcpListener.EndAcceptSocket(ar));
                } catch (ObjectDisposedException ex) {
                    log.Debug("TODO is there a better way to handle this", ex);
                    return;
                } finally {
                    _acceptThreadLoopEvent.Set();
                }

                if (log.IsDebugEnabled) {
                    log.DebugFormat(
                        "connection accepted [{0}:{1}] => [{2}:{3}]",
                        clientSocket.LocalAddress.ToString(), clientSocket.LocalPort,
                        clientSocket.RemoteAddress.ToString(), clientSocket.RemotePort);
                }
                state = _server.CreateState(this, clientSocket);
                _server.FireInitializeState(state);
                state.FireConnectionOpened();
                clientSocket.BeginReceive(state.ReceiveBuffer, 0, state.ReceiveBuffer.Length, SocketFlags.Partial, ReceiveCallback, state);
            } catch (Exception ex) {
                log.Error("AcceptSocket", ex);
                if (state != null) {
                    state.FireError(ex);
                }
            }
        }

        private static void ReceiveCallback(IAsyncResult ar) {
            IState state = (IState)ar.AsyncState;
            try {
                state.FireAttachThread(Thread.CurrentThread);
                SocketError errorCode;
                int numberOfBytesReceived = state.ClientSocket.EndReceive(ar, out errorCode);
                if (numberOfBytesReceived == 0) {
                    state.CloseConnection = true;
                }
                state.FireDataReceived(numberOfBytesReceived);
                if (state.CloseConnection) {
                    state.ClientSocket.BeginDisconnect(false, DisconnectCallback, state);
                } else {
                    state.ClientSocket.BeginReceive(state.ReceiveBuffer, 0, state.ReceiveBuffer.Length,
                                                    SocketFlags.Partial, ReceiveCallback, state);
                }
            } catch (Exception ex) {
                log.Error("ReceiveCallback", ex);
                state.FireError(ex);
            } finally {
                state.FireDetachThread(Thread.CurrentThread);
            }
        }

        private static void DisconnectCallback(IAsyncResult ar) {
            IState state = (IState)ar.AsyncState;
            try {
                state.ClientSocket.Close();
                state.FireConnectionClosed();
            } catch (Exception ex) {
                log.Error("DisconnectCallback", ex);
                state.FireError(ex);
            }
        }

        /// <summary>
        /// Stops the listener.
        /// </summary>
        public void Stop() {
            if (Started) {
                _acceptConnections = false;
                _acceptThreadLoopEvent.Set();
                _tcpListener.Stop();

                _acceptThreadLoop.Join();
                _started = false;
            }
        }

        /// <summary>
        /// Sends data to client.
        /// </summary>
        public void Send(IState state, string data) {
            byte[] buffer = ASCIIEncoding.ASCII.GetBytes(data);
            Send(state, new MemoryStream(buffer));
        }

        /// <summary>
        /// sends a stream of data to the client.
        /// </summary>
        public void Send(IState state, Stream stream) {
            SendDataState sds = new SendDataState(state, stream);
            int read = stream.Read(sds.Buffer, 0, sds.Buffer.Length);
            state.ClientSocket.BeginSend(sds.Buffer, 0, read, SocketFlags.None, SendCallback, sds);
        }

        /// <summary>
        /// creates a socket to a given address and port.
        /// </summary>
        public ISocket CreateSocket() {
            ISocket results = new TcpSocket();
            return results;
        }

        private static void SendCallback(IAsyncResult ar) {
            SendDataState sds = (SendDataState)ar.AsyncState;
            try {
                sds.State.FireAttachThread(Thread.CurrentThread);
                SocketError errorCode;
                int sent = sds.State.ClientSocket.EndSend(ar, out errorCode);
                log.DebugFormat("data [sent bytes: {0}] [error code: {1}]", sent, errorCode);
                int read = sds.Stream.Read(sds.Buffer, 0, sds.Buffer.Length);
                if (read > 0) {
                    sds.State.ClientSocket.BeginSend(sds.Buffer, 0, read, SocketFlags.None, SendCallback, sds);
                }
            } catch (Exception ex) {
                log.Error("SendCallback", ex);
                sds.State.FireError(ex);
            } finally {
                sds.State.FireDetachThread(Thread.CurrentThread);
            }
        }

        private class SendDataState {
            private readonly IState _state;
            private readonly Stream _stream;
            private readonly byte[] _buffer = new byte[10 * 1024];

            public SendDataState(IState state, Stream stream) {
                _state = state;
                _stream = stream;
            }

            public IState State {
                get { return _state; }
            }

            public Stream Stream {
                get { return _stream; }
            }

            public byte[] Buffer {
                get { return _buffer; }
            }
        }
    }
}
